#ifndef TESTCONVERTTOSTANDARDFORM_HPP
#define TESTCONVERTTOSTANDARDFORM_HPP

// TODO
#include <iostream>

#include <Eigen/Sparse>
#include <Eigen/Dense>

#include <gtest/gtest.h>
#include <gmock/gmock.h>

#include <LinearProgramming/Presolve.hpp>

// TODO Move as part of test utilities if used elsewhere
void fillSparseMatrix(Eigen::SparseMatrix<double>& sm) {
  for (int i = 0; i < sm.rows(); ++i) {
    // std::cout << sm.rows() << std::endl;
    for (int j = 0; j < sm.cols(); ++j) {
      sm.insert(i, j) = i + j;
    }
  }
}

/*!
 * \brief The MockMpsReader class
 *  Problem definition
 *    Unknowns are x1, x2, x3
 *  Objective function
 *  1*x1 + 2*x2 + 3*x3
 *  Constraint Matrix with RHS ( <= or < both result in same final result)
 *  0, 1, 2 > 1
 *  1, 2, 3 < 2
 *  2, 3, 4 = 3
 *
 *  Bounds are defined differently for each test case
 *  0 <= x1 <= 2; -2 <= x2 <= 0; -2 <= x3 <= 2;
 *
 *  Expected output
 *
 *
 */
class MockMpsReader {
 public:

  enum class RowSense : char {
    LESS_THAN, GREATER_THAN, EQUAL_TO
  };

  MOCK_CONST_METHOD0(getObjectiveCoefficients, Eigen::SparseVector<double>());
  MOCK_CONST_METHOD0(getRHS, Eigen::SparseVector<double>());
  MOCK_CONST_METHOD0(getConstraints, Eigen::SparseMatrix<double>());
  MOCK_CONST_METHOD0(getRowSense, std::vector<RowSense>());

  MOCK_CONST_METHOD0(getColumnLowerBounds, std::vector<double>());
  MOCK_CONST_METHOD0(getColumnUpperBounds, std::vector<double>());
};

class TestColumnStandardization : public testing::Test {

 public:
  TestColumnStandardization() {
    using testing::Return;

    {
      Eigen::SparseVector<double> rhs(3);

      rhs.insertBack(0) = 1;
      rhs.insertBack(1) = 2;
      rhs.insertBack(2) = 3;

      EXPECT_CALL(mpsReader, getRHS()).Times(1).WillOnce(Return(rhs));
    }

    {
      Eigen::SparseVector<double> coefficients(3);

      coefficients.insertBack(0) = 1;
      coefficients.insertBack(1) = 2;
      coefficients.insertBack(2) = 3;

      EXPECT_CALL(mpsReader, getObjectiveCoefficients()).Times(1)
          .WillOnce(Return(coefficients));
    }

    {
      Eigen::SparseMatrix<double> constraints(3, 3);

      // 0, 1, 2
      // 1, 2, 3
      // 2, 3, 4
      fillSparseMatrix(constraints);

      EXPECT_CALL(mpsReader, getConstraints()).Times(1)
          .WillOnce(Return(constraints));
    }

    {
      std::vector<MockMpsReader::RowSense> rowSense;

      rowSense.emplace_back(MockMpsReader::RowSense::GREATER_THAN);
      rowSense.emplace_back(MockMpsReader::RowSense::LESS_THAN);
      rowSense.emplace_back(MockMpsReader::RowSense::EQUAL_TO);

      EXPECT_CALL(mpsReader, getRowSense()).Times(1).WillOnce(Return(rowSense));
    }
  }

 protected:
  MockMpsReader mpsReader;

  green::Presolve<MockMpsReader> getConverter() {
    return green::Presolve<MockMpsReader>(mpsReader);
  }
};

// Start testing
TEST_F(TestColumnStandardization, TestColumnInterval) {

  using testing::Return;

  auto converter = getConverter();

  // All three kinds of intervals added
  {
    std::vector<double> lowerBounds{0, -2, -2};

    EXPECT_CALL(mpsReader, getColumnLowerBounds()).Times(1)
        .WillOnce(Return(lowerBounds));
  }

  {
    std::vector<double> upperBounds{2, 0, 2};

    EXPECT_CALL(mpsReader, getColumnUpperBounds()).Times(1)
        .WillOnce(Return(upperBounds));
  }

  converter.standardize();
  auto constraints = converter.getConstraints();

  //#include <iostream>
  // std::cout << constraints << std::endl;
  {
    // Test size of matrix
    EXPECT_EQ(constraints.rows(), 6);
    EXPECT_EQ(constraints.cols(), 8);
  }

  {
    // Test added rows 1 by 1
    Eigen::SparseVector<double> expectedRow1(8);
    Eigen::SparseVector<double> expectedRow2(8);
    Eigen::SparseVector<double> expectedRow3(8);
    Eigen::SparseVector<double> expectedRow4(8);
    Eigen::SparseVector<double> expectedRow5(8);
    Eigen::SparseVector<double> expectedRow6(8);

    expectedRow1.insertBack(0) = 1;
    expectedRow1.insertBack(1) = 2;
    expectedRow1.insertBack(2) = 3;
    expectedRow1.insertBack(3) = 0;
    expectedRow1.insertBack(4) = 1;
    expectedRow1.insertBack(5) = 0;
    expectedRow1.insertBack(6) = 0;
    expectedRow1.insertBack(7) = 0;

    expectedRow2.insertBack(0) = 2;
    expectedRow2.insertBack(1) = 3;
    expectedRow2.insertBack(2) = 4;
    expectedRow2.insertBack(3) = 0;
    expectedRow2.insertBack(4) = 0;
    expectedRow2.insertBack(5) = 0;
    expectedRow2.insertBack(6) = 0;
    expectedRow2.insertBack(7) = 0;

    expectedRow3.insertBack(0) = 1;
    expectedRow3.insertBack(1) = 0;
    expectedRow3.insertBack(2) = 0;
    expectedRow3.insertBack(3) = 0;
    expectedRow3.insertBack(4) = 0;
    expectedRow3.insertBack(5) = 1;
    expectedRow3.insertBack(6) = 0;
    expectedRow3.insertBack(7) = 0;

    expectedRow4.insertBack(0) = 0;
    expectedRow4.insertBack(1) = 1;
    expectedRow4.insertBack(2) = 2;
    expectedRow4.insertBack(3) = -1;
    expectedRow4.insertBack(4) = 0;
    expectedRow4.insertBack(5) = 0;
    expectedRow4.insertBack(6) = 0;
    expectedRow4.insertBack(7) = 0;

    expectedRow5.insertBack(0) = 0;
    expectedRow5.insertBack(1) = 1;
    expectedRow5.insertBack(2) = 0;
    expectedRow5.insertBack(3) = 0;
    expectedRow5.insertBack(4) = 0;
    expectedRow5.insertBack(5) = 0;
    expectedRow5.insertBack(6) = 1;
    expectedRow5.insertBack(7) = 0;

    expectedRow6.insertBack(0) = 0;
    expectedRow6.insertBack(1) = 0;
    expectedRow6.insertBack(2) = 1;
    expectedRow6.insertBack(3) = 0;
    expectedRow6.insertBack(4) = 0;
    expectedRow6.insertBack(5) = 0;
    expectedRow6.insertBack(6) = 0;
    expectedRow6.insertBack(7) = 1;

    // Eigen does not allow to have row access on column major matrices for
    // performance reasons
    // TODO Is there a better way to convert from rowMajor to colMajor
    // Look at
    // http://forum.kde.org/viewtopic.php?f=74&t=112018&sid=ef0e09f9695cf39567974ff8c3f61313
    Eigen::SparseMatrix<double, Eigen::RowMajor> constraintsRowMajor =
        constraints;

    EXPECT_EQ(expectedRow1, constraintsRowMajor.row(0).transpose());
    EXPECT_EQ(expectedRow2, constraintsRowMajor.row(1).transpose());
    EXPECT_EQ(expectedRow3, constraintsRowMajor.row(2).transpose());
    EXPECT_EQ(expectedRow4, constraintsRowMajor.row(3).transpose());
    EXPECT_EQ(expectedRow5, constraintsRowMajor.row(4).transpose());
    EXPECT_EQ(expectedRow6, constraintsRowMajor.row(5).transpose());
  }

  {
    // Test RHS
    auto rhs = converter.getRHS();

    Eigen::SparseVector<double> expectedRHS(6);
    expectedRHS.insertBack(0) = 12;
    expectedRHS.insertBack(1) = 17;
    expectedRHS.insertBack(2) = 2;
    expectedRHS.insertBack(3) = 7;
    expectedRHS.insertBack(4) = 2;
    expectedRHS.insertBack(5) = 4;

    EXPECT_EQ(expectedRHS, rhs);
  }

  {
    // Test objective, ofcourse this should not change
    auto objective = converter.getObjective();

    Eigen::SparseVector<double> expectedObjective(3);

    expectedObjective.insertBack(0) = 1;
    expectedObjective.insertBack(1) = 2;
    expectedObjective.insertBack(2) = 3;

    EXPECT_EQ(expectedObjective, objective);
  }
}

TEST_F(TestColumnStandardization, TestColumnFree) {

  using testing::Return;

  auto converter = getConverter();

  {
    std::vector<double> lowerBounds{0, std::numeric_limits<double>::lowest(),
                                    0};

    EXPECT_CALL(mpsReader, getColumnLowerBounds()).Times(1)
        .WillOnce(Return(lowerBounds));
  }

  {
    std::vector<double> upperBounds{std::numeric_limits<double>::max(),
                                    std::numeric_limits<double>::max(),
                                    std::numeric_limits<double>::max()};

    EXPECT_CALL(mpsReader, getColumnUpperBounds()).Times(1)
        .WillOnce(Return(upperBounds));
  }

  converter.standardize();
  auto constraints = converter.getConstraints();
  {
    // Check size of matrix
    EXPECT_EQ(constraints.rows(), 3);
    EXPECT_EQ(constraints.cols(), 6);
    EXPECT_EQ(constraints.size(), 18);
  }
  {
    Eigen::SparseVector<double> expectedColumn(3);

    expectedColumn.insertBack(0) = -1;
    expectedColumn.insertBack(1) = -2;
    expectedColumn.insertBack(2) = -3;

    Eigen::SparseVector<double> originalColumn(constraints.middleCols<1>(3));
    // Check added column is correct or not
    EXPECT_EQ(expectedColumn, originalColumn);
  }

  {
    auto objective = converter.getObjective();

    Eigen::SparseVector<double> expectedObjective(4);

    expectedObjective.insertBack(0) = 1;
    expectedObjective.insertBack(1) = 2;
    expectedObjective.insertBack(2) = 3;
    expectedObjective.insertBack(3) = -2;
    // Check added row in coefficient matrix is correct
    EXPECT_EQ(expectedObjective, objective);
  }
}

TEST_F(TestColumnStandardization, TestColumnLowerBound) {

  using testing::Return;

  auto converter = getConverter();

  {
    std::vector<double> lowerBounds{0, -2, 0};

    EXPECT_CALL(mpsReader, getColumnLowerBounds()).Times(1)
        .WillOnce(Return(lowerBounds));
  }

  {
    std::vector<double> upperBounds{std::numeric_limits<double>::max(),
                                    std::numeric_limits<double>::max(),
                                    std::numeric_limits<double>::max()};

    EXPECT_CALL(mpsReader, getColumnUpperBounds()).Times(1)
        .WillOnce(Return(upperBounds));
  }

  converter.standardize();
  {
    // Test RHS
    auto rhs = converter.getRHS();

    Eigen::SparseVector<double> expectedRHS(3);

    expectedRHS.insertBack(0) = 3;
    expectedRHS.insertBack(1) = 6;
    expectedRHS.insertBack(2) = 9;

    EXPECT_EQ(expectedRHS, rhs);
  }

  {
    // Size of constraints remains same
    auto constraints = converter.getConstraints();

    EXPECT_EQ(constraints.rows(), 3);
    EXPECT_EQ(constraints.cols(), 5);
  }
}

TEST_F(TestColumnStandardization, TestColumnUpperBound) {

  using testing::Return;

  auto converter = getConverter();

  {
    std::vector<double> lowerBounds{0, std::numeric_limits<double>::lowest(),
                                    0};

    EXPECT_CALL(mpsReader, getColumnLowerBounds()).Times(1)
        .WillOnce(Return(lowerBounds));
  }

  {
    std::vector<double> upperBounds{std::numeric_limits<double>::max(), 2,
                                    std::numeric_limits<double>::max()};

    EXPECT_CALL(mpsReader, getColumnUpperBounds()).Times(1)
        .WillOnce(Return(upperBounds));
  }

  converter.standardize();
  {
    // Test RHS
    auto rhs = converter.getRHS();

    Eigen::SparseVector<double> expectedRHS(3);

    expectedRHS.insertBack(0) = -1;
    expectedRHS.insertBack(1) = -2;
    expectedRHS.insertBack(2) = -3;

    EXPECT_EQ(expectedRHS, rhs);
  }

  {
    // Size of constraints remains same
    auto constraints = converter.getConstraints();

    EXPECT_EQ(constraints.rows(), 3);
    EXPECT_EQ(constraints.cols(), 5);

    Eigen::SparseVector<double> expectedColumn(3);

    expectedColumn.insertBack(0) = -1;
    expectedColumn.insertBack(1) = -2;
    expectedColumn.insertBack(2) = -3;

    EXPECT_EQ(expectedColumn, constraints.col(1));
  }

  {

    auto objective = converter.getObjective();

    Eigen::SparseVector<double> expectedObjective(3);

    expectedObjective.insertBack(0) = 1;
    expectedObjective.insertBack(1) = -2;
    expectedObjective.insertBack(2) = 3;

    EXPECT_EQ(expectedObjective, objective);
  }
}

class TestFullRowRank : public testing::Test {

 public:
  TestFullRowRank() {
    using testing::Return;

    {
      Eigen::SparseVector<double> rhs(5);

      rhs.insertBack(0) = 1;
      rhs.insertBack(1) = 2;
      rhs.insertBack(2) = 3;
      rhs.insertBack(3) = 4;
      rhs.insertBack(4) = 5;

      EXPECT_CALL(mpsReader, getRHS()).Times(1).WillOnce(Return(rhs));
    }

    {
      Eigen::SparseVector<double> coefficients(3);

      coefficients.insertBack(0) = 1;
      coefficients.insertBack(1) = 2;
      coefficients.insertBack(2) = 3;

      EXPECT_CALL(mpsReader, getObjectiveCoefficients()).Times(1)
          .WillOnce(Return(coefficients));
    }

    {
      Eigen::SparseMatrix<double> constraints(3, 3);

      // 0, 1, 2
      // 1, 2, 3
      // 2, 3, 4
      fillSparseMatrix(constraints);
      // Add tow new dependent rows, so final soze is 5 X 3
      // 1. Fourth row is combination of 1 and 2 rows
      // 2. Fifth row is combination of 1 and 3 rows
      constraints.conservativeResize(5, 3);
      // Changing value in 3rd row to avoid depenedent row in generated rows
      // above
      constraints.coeffRef(2, 1) = 5;

      constraints.insert(3, 0) = 1;
      constraints.insert(3, 1) = 3;
      constraints.insert(3, 2) = 5;

      constraints.insert(4, 0) = 2;
      constraints.insert(4, 1) = 4;
      constraints.insert(4, 2) = 6;

      EXPECT_CALL(mpsReader, getConstraints()).Times(1)
          .WillOnce(Return(constraints));
    }

    {
      std::vector<MockMpsReader::RowSense> rowSense;

      rowSense.emplace_back(MockMpsReader::RowSense::EQUAL_TO);
      rowSense.emplace_back(MockMpsReader::RowSense::EQUAL_TO);
      rowSense.emplace_back(MockMpsReader::RowSense::EQUAL_TO);
      rowSense.emplace_back(MockMpsReader::RowSense::EQUAL_TO);
      rowSense.emplace_back(MockMpsReader::RowSense::EQUAL_TO);

      EXPECT_CALL(mpsReader, getRowSense()).Times(1).WillOnce(Return(rowSense));
    }

    {
      std::vector<double> lowerBounds{0, 0, 0};

      EXPECT_CALL(mpsReader, getColumnLowerBounds()).Times(1)
          .WillOnce(Return(lowerBounds));
    }

    {
      std::vector<double> upperBounds{std::numeric_limits<double>::max(),
                                      std::numeric_limits<double>::max(),
                                      std::numeric_limits<double>::max()};

      EXPECT_CALL(mpsReader, getColumnUpperBounds()).Times(1)
          .WillOnce(Return(upperBounds));
    }
  }

 protected:
  MockMpsReader mpsReader;

  green::Presolve<MockMpsReader> getConverter() {
    return green::Presolve<MockMpsReader>(mpsReader);
  }
};

TEST_F(TestFullRowRank, TestFullRowRankMethod) {

  using testing::Return;

  auto converter = getConverter();

  converter.standardize();

  {
    // Test RHS
    auto rhs = converter.getRHS();

    Eigen::SparseVector<double> expectedRHS(3);

    expectedRHS.insertBack(0) = 1;
    expectedRHS.insertBack(1) = 2;
    expectedRHS.insertBack(2) = 3;

    EXPECT_EQ(expectedRHS, rhs);
  }

  {
    auto constraints = converter.getConstraints();

    EXPECT_EQ(constraints.rows(), 3);
    EXPECT_EQ(constraints.cols(), 3);
  }

  {
    // This would not change in this scenario
    auto objective = converter.getObjective();

    Eigen::SparseVector<double> expectedObjective(3);

    expectedObjective.insertBack(0) = 1;
    expectedObjective.insertBack(1) = 2;
    expectedObjective.insertBack(2) = 3;

    EXPECT_EQ(expectedObjective, objective);
  }
}

#endif  // TESTCONVERTTOSTANDARDFORM_HPP
